package com.xxxx.seckill.controller;





import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.xxxx.seckill.exception.GlobalException;
import com.xxxx.seckill.pojo.Order;

import com.xxxx.seckill.pojo.SeckillMessage;
import com.xxxx.seckill.pojo.SeckillOrder;
import com.xxxx.seckill.pojo.User;
import com.xxxx.seckill.rocketmq.MQProducer;
import com.xxxx.seckill.service.IGoodsService;
import com.xxxx.seckill.service.IOrderService;
import com.xxxx.seckill.service.ISeckillOrderService;
import com.xxxx.seckill.vo.GoodsVo;
import com.xxxx.seckill.vo.RespBean;
import com.xxxx.seckill.vo.RespBeanEnum;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.map.LRUMap;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;


import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


/**
 * 秒杀页面  QPS 1213
 * 缓存的QPS 1227
 * 优化之后：2348  直接翻了两倍哦
 *
 * @author LiXuekai on 2022/3/29
 * @time 8:54
 */
@Controller
@RequestMapping("/seckill")
public class SeckillController implements InitializingBean {

    @Autowired
    private IGoodsService goodsService;

    @Autowired
    private ISeckillOrderService orderService;
    @Autowired
    private IOrderService iOrderService;
    @Autowired
    private RedisTemplate redisTemplate;
    @Autowired
    private MQProducer mqProducer;
    @Autowired
    private RedisScript<Long> redisScript;

    private Map<Long, Boolean> map = new HashMap<>();


    /**
     * qps 1164
     * 秒杀
     * @param model
     * @param user
     * @param goodsId
     * @return
     */
    @RequestMapping("/doSeckill2")

    public String doSeckill2(Model model, User user, Long goodsId) {
        if (user == null) {
            return "login";
        }
        model.addAttribute("user", user);
        GoodsVo goods = goodsService.findGoodsVoByGoodsId(goodsId);

        if (goods.getStockCount() < 1) {
            //静态化之后是不用在加进去的
            //model.addAttribute("errmsg", RespBeanEnum.EMPTY_STOCK.getMessage());
            return "secKillFail";
        }


        //判断是否进行重复抢购，我也可以用lrumap 实现的? 可不可以真不好说,或者用下面的redis来实现
        SeckillOrder seckillOrder = orderService.getOne(new QueryWrapper<SeckillOrder>()
                .eq("user_id", user.getId()).eq("goods_id", goodsId));
        //这里用redis去判断一个用户是否进行了重复下单的问题


        if (seckillOrder != null) {
            //model.addAttribute("errmsg", RespBeanEnum.REPEATE_ERROR.getMessage());
            return "secKillFail";
        }
        //将这个订单包装起来，seckill方法是将订单和用户信息放进数据库，然后返回一个order

        //这个地方可能存在优化的点，注意一下，压测改个顺序试一试 ->
        Order order = iOrderService.seckill(user, goods);


        //传给前端
        model.addAttribute("order", order);
        model.addAttribute("goods", goods);
        return "orderDetail";


    }



    /**
     * 秒杀
     *
     * @param path
     * @param user
     * @param goodsId
     * @return
     */
    @RequestMapping(value = "/{path}/doSeckill", method = RequestMethod.POST)
    @ResponseBody
    public RespBean doSeckill(User user, Long goodsId,@PathVariable String path) {
        if (user == null) {
            return RespBean.error(RespBeanEnum.ERROR);
        }
        /*GoodsVo goods = goodsService.findGoodsVoByGoodsId(goodsId);

        //判断库存
        if (goods.getStockCount() < 1) {
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }

        //mysql判断是否进行重复抢购，我也可以用lrumap 实现的? 可不可以真不好说
        //SeckillOrder seckillOrder = orderService.getOne(new QueryWrapper<SeckillOrder>()
                //.eq("user_id", user.getId()).eq("goods_id", goodsId));

        //这里用redis去判断一个用户是否进行了重复下单的问题
        //redis是否重复抢购
        SeckillOrder seckillOrder = (SeckillOrder)redisTemplate
                .opsForValue()
                .get("order:" + user.getId() + ":" + goodsId);

        if (seckillOrder != null) {
            return RespBean.error(RespBeanEnum.REPEATE_ERROR);
        }

        //将这个订单包装起来，seckill方法是将订单和用户信息放进数据库，然后返回一个order
        Order order = iOrderService.seckill(user, goods);

        return RespBean.success(order);*/


        ValueOperations valueOperations = redisTemplate.opsForValue();

        //校验请求地址是否合法
        boolean check = iOrderService.check(user, goodsId,path);
        if (!check) {
            return RespBean.error(RespBeanEnum.REQYUEST_ILLEGAL);
        }

        //redis是否重复抢购，也就是这个key是否存在
        SeckillOrder seckillOrder = (SeckillOrder) redisTemplate
                .opsForValue()
                .get("order:" + user.getId() + ":" + goodsId);

        if (seckillOrder != null) {
            return RespBean.error(RespBeanEnum.REPEATE_ERROR);
        }

        //内存标记减少redis的访问
        //已经没了
        if (map.get(goodsId)) {
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }


        //预减库存
        //递减 递减之后的库存
        //Long decrement = valueOperations.decrement("seckillGoods" + goodsId);
        //使用lua脚本来实现redis减库存
        Long decrement = (Long) redisTemplate.execute(redisScript, Collections.singletonList("seckillGoods" + goodsId), Collections.EMPTY_LIST);

        /**
         * 这里等会要改一下 ->改完了
         */
        if (decrement < 0) {
            //先标记内存
            map.put(goodsId, true);
            //这里加一次是为了防止其变为-1了，就不好了
            //valueOperations.increment("seckillGoods" + goodsId);
            return RespBean.error(RespBeanEnum.EMPTY_STOCK);
        }

        //将这个订单包装起来，seckill方法是将订单和用户信息放进数据库，然后返回一个order
        //Order order = iOrderService.seckill(user, goods);

        //包装消息
        SeckillMessage seckillMessage = new SeckillMessage(user, goodsId);

        //MQ发送消息
        mqProducer.sendSeckillMessage(JSON.toJSONString(seckillMessage));


        return RespBean.success(0);

    }

    /**
     * 获取秒杀结果
     * @param goodsId
     * @return orderId:成功， -1:秒杀失败   0：排队中
     */
    @RequestMapping(value = "/result", method = RequestMethod.GET)
    @ResponseBody
    public RespBean getResult(User user, Long goodsId) {
        if (user == null) {
            return RespBean.error(RespBeanEnum.SESSION_ERROR);
        }
        //Long orderId = orderService.getResult(user, goodsId);
        //直接从redis中去查他不香吗？难道是为了保证数据库一致性问题？没必要吧啊
        SeckillOrder seckillOrder = (SeckillOrder)redisTemplate.opsForValue().get("order:" + user.getId() + ":" + goodsId);
        if (null != seckillOrder) {
            return RespBean.success(seckillOrder.getOrderId());
        } else if (redisTemplate.hasKey("isStockEmpty:" + goodsId)) {
            return RespBean.success(-1L);
        } else {
            return RespBean.success(0L);
        }

    }

    /**
     * 生成一个随机的url
     * @param user
     * @param goodsId
     * @return
     */
    @RequestMapping(value = "/path", method = RequestMethod.GET)
    @ResponseBody
    public RespBean getPath(User user,Long goodsId){
        if (user == null) {
            return RespBean.error(RespBeanEnum.SESSION_ERROR);
        }

        String str = iOrderService.createPath(user, goodsId);
        return RespBean.success(str);
    }
    /**
     * 初始化时候可以执行的方法,把商品的数量预加载到redis中去
     *
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        //首先你要拿到库存吧对不对
        List<GoodsVo> goodsVoList = goodsService.findGoodsVo();

        if (CollectionUtils.isEmpty(goodsVoList)) {
            return;
        }
        //加载到redis中去
        goodsVoList.forEach(goodsVo -> {
            redisTemplate.opsForValue().set("seckillGoods" + goodsVo.getId(), goodsVo.getStockCount());
            map.put(goodsVo.getId(), false);
        });
    }
}
